/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distlributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkAssignStmt, ArkInstanceInvokeExpr, ArkInvokeStmt, ArkMethod, ArkParameterRef, ArkStaticInvokeExpr, DEFAULT_ARK_CLASS_NAME, Local, Scene, Stmt, StringType, Value } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { CheckerUtils, Defects, MatcherCallback, MatcherTypes, MethodMatcher, Rule } from '../../Index';
import { IssueReport } from '../../model/Defects';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { hilogPrintSignList } from '../../utils/checker/HilogConstants';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'LazyStringCheckInLog');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/lazy-string-check-in-log.md',
    description: 'Use lazy evaluation for string parameters in logging functions when possible.'
};

interface logOption {
    filePath: string;
    className: string;
    methodName: string[];
}

interface StackItem {
    method: ArkMethod;
    processed: boolean;
}

export class LazyStringCheckInLog implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private metMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD
    };

    public registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: this.metMatcher,
            callback: this.check
        };
        return [matchFileCb];
    }

    public check = (arkMethod: ArkMethod): void => {
        let option = this.rule.option[0] as logOption;
        let scene = arkMethod.getDeclaringArkFile().getScene();
        if (option) {
            this.checkForProvision(option, arkMethod);
        } else {
            this.processMethod(arkMethod, scene);
        }
    };

    private checkForProvision(option: logOption, arkMethod: ArkMethod): void {
        const stmts = arkMethod.getBody()?.getCfg()?.getStmts() ?? [];
        for (let stmt of stmts) {
            if (!(stmt instanceof ArkInvokeStmt)) {
                continue;
            }
            let invoker = stmt.getInvokeExpr();
            let fileName = invoker.getMethodSignature().getDeclaringClassSignature().getDeclaringFileSignature().getFileName();
            let className = invoker.getMethodSignature().getDeclaringClassSignature().getClassName();
            let methodName = invoker.getMethodSignature().getMethodSubSignature().getMethodName();
            if (option.filePath.includes(fileName) && option.className === className && option.methodName.includes(methodName)) {
                for (let arg of invoker.getArgs()) {
                    this.alarmReport(arg, stmt);
                }
            }
        }
    }

    private processMethod(method: ArkMethod, scene: Scene): void {
        let stack: StackItem[] = [];
        let visitedSignatures = new Set<string>();
        stack.push({ method, processed: false });
        while (stack.length > 0) {
            const lastElement = stack.pop();
            if (!lastElement) {
                continue;
            }
            if (!lastElement.processed) {
                let signature = lastElement.method.getSignature().toString();
                if (visitedSignatures.has(signature)) {
                    continue;
                }
                visitedSignatures.add(signature);
                this.processStmt(lastElement.method, stack, scene);
            }
        }
    }

    private processStmt(method: ArkMethod, stack: StackItem[], scene: Scene): void {
        for (let stmt of method.getBody()?.getCfg()?.getStmts() ?? []) {
            const invoker = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invoker) {
                continue;
            }
            if (hilogPrintSignList.some(pattern => invoker.getMethodSignature().toString().startsWith(pattern))) {
                let arg = invoker.getArg(3);
                this.alarmReport(arg, stmt);
            } else {
                let logMetnod = scene.getMethod(invoker.getMethodSignature());
                if (!logMetnod) {
                    continue;
                }
                let argArray = this.extractHilogArgs(logMetnod);
                if (argArray.length === 0) {
                    stack.push({ method: method, processed: false });
                }
                let locals = logMetnod.getBody()?.getLocals();
                if (!locals) {
                    continue;
                }
                let argIndex = this.matchLocal(locals, argArray);
                if (argIndex === null) {
                    continue;
                }
                let arg = invoker.getArg(argIndex);
                this.alarmReport(arg, stmt);
            }
        }
    }

    private matchLocal(locals: Map<string, Local>, argArray: Value[]): number | null {
        for (let [localName, local] of locals) {
            let decStmt = local.getDeclaringStmt();
            if (decStmt instanceof ArkAssignStmt) {
                let rightOp = decStmt.getRightOp();
                if (!(rightOp instanceof ArkParameterRef)) {
                    continue;
                }
                if (!argArray.includes(local)) {
                    continue;
                }
                return rightOp.getIndex();
            }
        }
        return null;
    }

    private alarmReport(arg: Value, stmt: Stmt): void {
        if (!(arg instanceof Local)) {
            return;
        }
        let decSubStmt = arg.getDeclaringStmt();
        let type = arg.getType();
        if (!(decSubStmt instanceof ArkAssignStmt)) {
            return;
        }
        let subRightOp = decSubStmt.getRightOp();
        if ((subRightOp instanceof ArkStaticInvokeExpr || subRightOp instanceof ArkInstanceInvokeExpr) && type instanceof StringType) {
            this.reportIssue(stmt, arg);
        }
    }

    private extractHilogArgs(method: ArkMethod): Value[] {
        let argArray: Value[] = [];
        for (let stmt of method.getBody()?.getCfg()?.getStmts() ?? []) {
            const invoker = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invoker) {
                continue;
            }
            if (hilogPrintSignList.some(pattern => invoker.getMethodSignature().toString().startsWith(pattern))) {
                let arg = invoker.getArg(3);
                argArray.push(arg);
            }
        }
        return argArray;
    }

    private reportIssue(stmt: Stmt, arg: Local): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getFilePath();
        const originPosition = stmt.getOperandOriginalPosition(arg);
        if (!originPosition) {
            return;
        }
        const lineNum = originPosition.getFirstLine();
        const startColumn = originPosition.getFirstCol();
        const endColunm = originPosition.getLastCol();
        let defects = new Defects(lineNum, startColumn, endColunm, this.metaData.description, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}
